สำรวจ hook useActionState ของ React สำหรับการจัดการ state ที่คล่องตัวซึ่งเกิดจากการทำงานแบบ asynchronous เพิ่มประสิทธิภาพและประสบการณ์ผู้ใช้ของแอปพลิเคชันคุณ
การใช้งาน React useActionState: การจัดการ State ตาม Action
hook useActionState ของ React ซึ่งเปิดตัวในเวอร์ชันล่าสุด นำเสนอแนวทางที่ปรับปรุงใหม่ในการจัดการการอัปเดต state ที่เป็นผลมาจากการทำงานแบบ asynchronous เครื่องมืออันทรงพลังนี้ช่วยลดความซับซ้อนของกระบวนการจัดการ mutations, การอัปเดต UI และการจัดการสถานะข้อผิดพลาด โดยเฉพาะอย่างยิ่งเมื่อทำงานร่วมกับ React Server Components (RSC) และ server actions คู่มือนี้จะสำรวจรายละเอียดของ useActionState พร้อมทั้งตัวอย่างที่นำไปใช้ได้จริงและแนวทางปฏิบัติที่ดีที่สุดสำหรับการนำไปใช้งาน
ทำความเข้าใจความจำเป็นของการจัดการ State ตาม Action
การจัดการ state แบบดั้งเดิมใน React มักเกี่ยวข้องกับการจัดการสถานะ loading และ error แยกกันภายใน components เมื่อมี action (เช่น การส่งฟอร์ม, การดึงข้อมูล) ที่กระตุ้นให้เกิดการอัปเดต state นักพัฒนามักจะจัดการสถานะเหล่านี้ด้วยการเรียกใช้ useState หลายครั้งและอาจมีตรรกะเงื่อนไขที่ซับซ้อน useActionState นำเสนอโซลูชันที่สะอาดและบูรณาการมากกว่า
ลองพิจารณาสถานการณ์การส่งฟอร์มง่ายๆ หากไม่มี useActionState คุณอาจต้องมี:
- ตัวแปร state สำหรับข้อมูลฟอร์ม
- ตัวแปร state เพื่อติดตามว่าฟอร์มกำลังส่งอยู่หรือไม่ (สถานะ loading)
- ตัวแปร state เพื่อเก็บข้อความแสดงข้อผิดพลาดใดๆ
แนวทางนี้อาจทำให้โค้ดเยิ่นเย้อและอาจเกิดความไม่สอดคล้องกัน useActionState รวบรวมข้อกังวลเหล่านี้ไว้ใน hook เดียว ทำให้ตรรกะง่ายขึ้นและปรับปรุงความสามารถในการอ่านโค้ด
แนะนำ useActionState
hook useActionState รับอาร์กิวเมนต์สองตัว:
- ฟังก์ชัน asynchronous (the "action") ที่ทำการอัปเดต state ซึ่งอาจเป็น server action หรือฟังก์ชัน asynchronous ใดๆ ก็ได้
- ค่า state เริ่มต้น
hook นี้จะคืนค่าเป็นอาร์เรย์ที่ประกอบด้วยสององค์ประกอบ:
- ค่า state ปัจจุบัน
- ฟังก์ชันสำหรับ dispatch action ฟังก์ชันนี้จะจัดการสถานะ loading และ error ที่เกี่ยวข้องกับ action โดยอัตโนมัติ
นี่คือตัวอย่างพื้นฐาน:
import { useActionState } from 'react';
async function updateServer(prevState, formData) {
// จำลองการอัปเดตเซิร์ฟเวอร์แบบ asynchronous
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
return 'Failed to update server.';
}
return `Updated name to: ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'Initial State');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
ในตัวอย่างนี้:
updateServerคือ action แบบ asynchronous ที่จำลองการอัปเดตเซิร์ฟเวอร์ โดยจะได้รับ state ก่อนหน้าและข้อมูลฟอร์มuseActionStateเริ่มต้น state ด้วย 'Initial State' และคืนค่า state ปัจจุบันและฟังก์ชันdispatch- ฟังก์ชัน
handleSubmitเรียกใช้dispatchพร้อมกับข้อมูลฟอร์มuseActionStateจะจัดการสถานะ loading และ error โดยอัตโนมัติระหว่างการดำเนินการของ action
การจัดการสถานะ Loading และ Error
หนึ่งในประโยชน์หลักของ useActionState คือการจัดการสถานะ loading และ error ที่มีมาในตัว ฟังก์ชัน dispatch จะคืนค่าเป็น promise ที่จะ resolve พร้อมกับผลลัพธ์ของ action หาก action เกิดข้อผิดพลาด (throws an error) promise จะ reject พร้อมกับข้อผิดพลาดนั้น คุณสามารถใช้สิ่งนี้เพื่ออัปเดต UI ได้ตามความเหมาะสม
แก้ไขตัวอย่างก่อนหน้านี้เพื่อแสดงข้อความ loading และข้อความ error:
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// จำลองการอัปเดตเซิร์ฟเวอร์แบบ asynchronous
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Failed to update server.');
}
return `Updated name to: ${data.name}`;
}
function MyComponent() {
const [state, dispatch] = useActionState(updateServer, 'Initial State');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
setIsSubmitting(true);
setErrorMessage(null);
try {
const result = await dispatch(formData);
console.log(result);
} catch (error) {
console.error("Error during submission:", error);
setErrorMessage(error.message);
} finally {
setIsSubmitting(false);
}
}
return (
);
}
การเปลี่ยนแปลงที่สำคัญ:
- เราได้เพิ่มตัวแปร state
isSubmittingและerrorMessageเพื่อติดตามสถานะ loading และ error - ใน
handleSubmitเราตั้งค่าisSubmittingเป็นtrueก่อนเรียกdispatchและดักจับข้อผิดพลาดใดๆ เพื่ออัปเดตerrorMessage - เราปิดใช้งานปุ่ม submit ในขณะที่กำลังส่งข้อมูล และแสดงข้อความ loading และ error ตามเงื่อนไข
useActionState กับ Server Actions ใน React Server Components (RSC)
useActionState จะโดดเด่นมากเมื่อใช้กับ React Server Components (RSC) และ server actions โดย Server actions คือฟังก์ชันที่ทำงานบนเซิร์ฟเวอร์และสามารถแก้ไขแหล่งข้อมูลได้โดยตรง ทำให้คุณสามารถดำเนินการฝั่งเซิร์ฟเวอร์ได้โดยไม่ต้องเขียน API endpoints
หมายเหตุ: ตัวอย่างนี้ต้องการสภาพแวดล้อม React ที่กำหนดค่าสำหรับ Server Components และ Server Actions
// app/actions.js (Server Action)
'use server';
import { cookies } from 'next/headers'; // ตัวอย่างสำหรับ Next.js
export async function updateName(prevState, formData) {
const name = formData.get('name');
if (!name) {
return 'Please enter a name.';
}
try {
// จำลองการอัปเดตฐานข้อมูล
await new Promise(resolve => setTimeout(resolve, 1000));
cookies().set('userName', name);
return `Updated name to: ${name}`; // สำเร็จ!
} catch (error) {
console.error("Database update failed:", error);
return 'Failed to update name.'; // สำคัญ: คืนค่าเป็นข้อความ ไม่ใช่โยน Error
}
}
// app/page.jsx (React Server Component)
'use client';
import { useActionState } from 'react';
import { updateName } from './actions';
function MyComponent() {
const [state, dispatch] = useActionState(updateName, 'Initial State');
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const result = await dispatch(formData);
console.log(result);
}
return (
);
}
export default MyComponent;
ในตัวอย่างนี้:
updateNameคือ server action ที่กำหนดไว้ในapp/actions.jsมันรับ state ก่อนหน้าและข้อมูลฟอร์ม, อัปเดตฐานข้อมูล (จำลอง) และคืนค่าข้อความสำเร็จหรือข้อความแสดงข้อผิดพลาด ที่สำคัญคือ action จะคืนค่าเป็นข้อความแทนที่จะโยน error (throw an error) Server Actions นิยมการคืนค่าข้อความที่ให้ข้อมูลมากกว่า- คอมโพเนนต์ถูกทำเครื่องหมายเป็น client component (
'use client') เพื่อใช้ hookuseActionState - ฟังก์ชัน
handleSubmitเรียกใช้dispatchพร้อมกับข้อมูลฟอร์มuseActionStateจะจัดการการอัปเดต state โดยอัตโนมัติตามผลลัพธ์ของ server action
ข้อควรพิจารณาที่สำคัญสำหรับ Server Actions
- การจัดการข้อผิดพลาดใน Server Actions: แทนที่จะโยนข้อผิดพลาด (throwing errors) ให้คืนค่าเป็นข้อความแสดงข้อผิดพลาดที่มีความหมายจาก Server Action ของคุณ
useActionStateจะถือว่าข้อความนี้เป็น state ใหม่ ซึ่งช่วยให้การจัดการข้อผิดพลาดฝั่งไคลเอนต์เป็นไปอย่างราบรื่น - Optimistic Updates: Server actions สามารถใช้กับการอัปเดตแบบมองโลกในแง่ดี (optimistic updates) เพื่อปรับปรุงประสิทธิภาพที่ผู้ใช้รับรู้ได้ คุณสามารถอัปเดต UI ได้ทันทีและย้อนกลับหาก action ล้มเหลว
- Revalidation: หลังจากการแก้ไขข้อมูลสำเร็จ ควรพิจารณา revalidate ข้อมูลที่แคชไว้เพื่อให้แน่ใจว่า UI สะท้อน state ล่าสุด
เทคนิค useActionState ขั้นสูง
1. การใช้ Reducer สำหรับการอัปเดต State ที่ซับซ้อน
สำหรับตรรกะ state ที่ซับซ้อนมากขึ้น คุณสามารถรวม useActionState เข้ากับฟังก์ชัน reducer ได้ วิธีนี้ช่วยให้คุณจัดการการอัปเดต state ในรูปแบบที่คาดเดาได้และบำรุงรักษาง่าย
import { useActionState } from 'react';
import { useReducer } from 'react';
const initialState = {
count: 0,
message: 'Initial State',
};
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_MESSAGE':
return { ...state, message: action.payload };
default:
return state;
}
}
async function updateState(state, action) {
// จำลองการทำงานแบบ asynchronous
await new Promise(resolve => setTimeout(resolve, 500));
switch (action.type) {
case 'INCREMENT':
return reducer(state, action);
case 'DECREMENT':
return reducer(state, action);
case 'SET_MESSAGE':
return reducer(state, action);
default:
return state;
}
}
function MyComponent() {
const [state, dispatch] = useActionState(updateState, initialState);
return (
Count: {state.count}
Message: {state.message}
);
}
2. Optimistic Updates กับ useActionState
Optimistic updates ช่วยปรับปรุงประสบการณ์ผู้ใช้โดยการอัปเดต UI ทันทีเสมือนว่า action นั้นสำเร็จแล้ว จากนั้นจึงย้อนกลับการอัปเดตหาก action ล้มเหลว ซึ่งจะทำให้แอปพลิเคชันของคุณรู้สึกตอบสนองได้ดียิ่งขึ้น
import { useActionState } from 'react';
import { useState } from 'react';
async function updateServer(prevState, formData) {
// จำลองการอัปเดตเซิร์ฟเวอร์แบบ asynchronous
await new Promise(resolve => setTimeout(resolve, 1000));
const data = Object.fromEntries(formData);
if (data.name === "error") {
throw new Error('Failed to update server.');
}
return `Updated name to: ${data.name}`;
}
function MyComponent() {
const [name, setName] = useState('Initial Name');
const [state, dispatch] = useActionState(async (prevName, newName) => {
try {
const result = await updateServer(prevName, {
name: newName,
});
return newName; // อัปเดตเมื่อสำเร็จ
} catch (error) {
// ย้อนกลับเมื่อเกิดข้อผิดพลาด
console.error("Update failed:", error);
setName(prevName);
return prevName;
}
}, name);
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const newName = formData.get('name');
setName(newName); // อัปเดต UI แบบมองโลกในแง่ดี
await dispatch(newName);
}
return (
);
}
3. การทำ Debouncing Actions
ในบางสถานการณ์ คุณอาจต้องการทำ debounce ให้กับ actions เพื่อป้องกันไม่ให้ถูก dispatch บ่อยเกินไป ซึ่งมีประโยชน์สำหรับสถานการณ์ต่างๆ เช่น ช่องค้นหาที่คุณต้องการทริกเกอร์ action หลังจากที่ผู้ใช้หยุดพิมพ์ไปแล้วช่วงหนึ่ง
import { useActionState } from 'react';
import { useState, useEffect } from 'react';
async function searchItems(prevState, query) {
// จำลองการค้นหาแบบ asynchronous
await new Promise(resolve => setTimeout(resolve, 500));
return `Search results for: ${query}`;
}
function MyComponent() {
const [query, setQuery] = useState('');
const [state, dispatch] = useActionState(searchItems, 'Initial State');
useEffect(() => {
const timeoutId = setTimeout(() => {
if (query) {
dispatch(query);
}
}, 300); // Debounce เป็นเวลา 300ms
return () => clearTimeout(timeoutId);
}, [query, dispatch]);
return (
setQuery(e.target.value)}
/>
State: {state}
);
}
แนวทางปฏิบัติที่ดีที่สุดสำหรับ useActionState
- รักษา Actions ให้ Pure: ตรวจสอบให้แน่ใจว่า actions ของคุณเป็น pure functions (หรือใกล้เคียงที่สุด) ไม่ควรมี side effects อื่นนอกเหนือจากการอัปเดต state
- จัดการข้อผิดพลาดอย่างนุ่มนวล: จัดการข้อผิดพลาดใน actions ของคุณเสมอและให้ข้อความแสดงข้อผิดพลาดที่ให้ข้อมูลแก่ผู้ใช้ ดังที่กล่าวไว้ข้างต้นกับ Server Actions ควรเลือกคืนค่าสตริงข้อความแสดงข้อผิดพลาดจาก server action แทนที่จะโยน error
- ปรับปรุงประสิทธิภาพ: คำนึงถึงผลกระทบด้านประสิทธิภาพของ actions ของคุณ โดยเฉพาะเมื่อต้องจัดการกับชุดข้อมูลขนาดใหญ่ พิจารณาใช้เทคนิค memoization เพื่อหลีกเลี่ยงการ re-render ที่ไม่จำเป็น
- พิจารณาการเข้าถึงได้ (Accessibility): ตรวจสอบให้แน่ใจว่าแอปพลิเคชันของคุณยังคงเข้าถึงได้สำหรับผู้ใช้ทุกคน รวมถึงผู้ที่มีความพิการด้วย จัดเตรียม ARIA attributes และการนำทางด้วยคีย์บอร์ดที่เหมาะสม
- การทดสอบอย่างละเอียด: เขียน unit tests และ integration tests เพื่อให้แน่ใจว่า actions และการอัปเดต state ของคุณทำงานอย่างถูกต้อง
- Internationalization (i18n): สำหรับแอปพลิเคชันระดับโลก ให้ใช้งาน i18n เพื่อรองรับหลายภาษาและวัฒนธรรม
- Localization (l10n): ปรับแต่งแอปพลิเคชันของคุณให้เข้ากับท้องถิ่นที่เฉพาะเจาะจงโดยการให้เนื้อหา รูปแบบวันที่ และสัญลักษณ์สกุลเงินที่เป็นภาษาท้องถิ่น
useActionState เทียบกับโซลูชันการจัดการ State อื่นๆ
แม้ว่า useActionState จะเป็นวิธีที่สะดวกในการจัดการการอัปเดต state ตาม action แต่มันไม่ใช่สิ่งที่จะมาแทนที่โซลูชันการจัดการ state ทั้งหมด สำหรับแอปพลิเคชันที่ซับซ้อนซึ่งมี global state ที่ต้องแชร์กันระหว่างหลาย components ไลบรารีอย่าง Redux, Zustand หรือ Jotai อาจเหมาะสมกว่า
เมื่อใดควรใช้ useActionState:
- การอัปเดต state ที่มีความซับซ้อนน้อยถึงปานกลาง
- การอัปเดต state ที่ผูกพันอย่างแน่นแฟ้นกับ actions แบบ asynchronous
- การทำงานร่วมกับ React Server Components และ Server Actions
เมื่อใดควรพิจารณาโซลูชันอื่น:
- การจัดการ global state ที่ซับซ้อน
- State ที่ต้องแชร์กันใน components จำนวนมาก
- คุณสมบัติขั้นสูง เช่น time-travel debugging หรือ middleware
บทสรุป
hook useActionState ของ React นำเสนอวิธีที่ทรงพลังและสวยงามในการจัดการการอัปเดต state ที่เกิดจาก actions แบบ asynchronous ด้วยการรวบรวมสถานะ loading และ error เข้าด้วยกัน มันช่วยให้โค้ดง่ายขึ้นและเพิ่มความสามารถในการอ่าน โดยเฉพาะอย่างยิ่งเมื่อทำงานกับ React Server Components และ server actions การทำความเข้าใจจุดแข็งและข้อจำกัดของมันจะช่วยให้คุณสามารถเลือกแนวทางการจัดการ state ที่เหมาะสมสำหรับแอปพลิเคชันของคุณ ซึ่งจะนำไปสู่โค้ดที่บำรุงรักษาง่ายและมีประสิทธิภาพมากขึ้น
ด้วยการปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดที่ระบุไว้ในคู่มือนี้ คุณสามารถใช้ประโยชน์จาก useActionState ได้อย่างมีประสิทธิภาพเพื่อปรับปรุงประสบการณ์ผู้ใช้และขั้นตอนการพัฒนาแอปพลิเคชันของคุณ อย่าลืมพิจารณาความซับซ้อนของแอปพลิเคชันและเลือกโซลูชันการจัดการ state ที่เหมาะสมกับความต้องการของคุณมากที่สุด ตั้งแต่การส่งฟอร์มง่ายๆ ไปจนถึงการแก้ไขข้อมูลที่ซับซ้อน useActionState สามารถเป็นเครื่องมือที่มีค่าในคลังแสงการพัฒนา React ของคุณได้